#pragma once

#include <vex/query/detail/where_adapter_body.h>
#include <vex/iterable/adapter/adapter_access.h>
#include <vex/iterable/adapter/adapter_default.h>
#include <boost/range/adaptor/argument_fwd.hpp>

namespace vex { namespace query { 
    
    namespace adapter {
        template<class TAdaptee, class TPredicate> struct where_adapter_traits {
            typedef typename std::decay<TAdaptee>::type adaptee_type;
            typedef typename std::decay<TPredicate>::type predicate_type;

            typedef iterable::range_traits<TAdaptee> adaptee_traits;

            // reference type of resulting range is unchanged
            typedef typename adaptee_traits::reference reference;

            // resulting category is restricted to at most bidirectional
            typedef typename std::conditional<
                std::is_convertible<typename adaptee_traits::category, iterable::random_access>::value,
                iterable::bidirectional,
                typename adaptee_traits::category
            >::type category;

            typedef detail::where_adapter_body<
                adaptee_type, 
                predicate_type, 
                typename std::conditional<
                std::is_convertible<category, iterable::bidirectional>::value,
                detail::geq_then_bidirectional,
                detail::less_then_bidirectional
                >::type
            > adapter_body_type;
        };

        template<class TAdaptee, class TPredicate> 
        class where_adapter : public iterable::range_base<
            typename where_adapter_traits<TAdaptee, TPredicate>::category,
            typename where_adapter_traits<TAdaptee, TPredicate>::reference,
            iterable::adapter::adapter_default<
            where_adapter<TAdaptee, TPredicate>,
            typename where_adapter_traits<TAdaptee, TPredicate>::category,
            typename where_adapter_traits<TAdaptee, TPredicate>::reference
            >
        >
        {
            typedef typename where_adapter_traits<TAdaptee, TPredicate>::adapter_body_type adaptee_type;

            friend class iterable::adapter::adapter_access;

            template<class, class> friend class where_adapter;

            adaptee_type m_adapter_body;
            adaptee_type& get_adaptee_ref() { return m_adapter_body; }
            adaptee_type const & get_adaptee_ref() const { return m_adapter_body; }

            where_adapter(adaptee_type p_where_adapter_body)
                : m_adapter_body(p_where_adapter_body) {}
        public:
            where_adapter(
                TAdaptee p_adaptee, 
                TPredicate p_predicate)
                : m_adapter_body(p_adaptee, p_predicate) {}

            where_adapter(where_adapter&& p_other)
                : m_adapter_body(std::move(p_other.m_adapter_body)) {}

            template<class URange>
            where_adapter(where_adapter<URange, TPredicate> const & p_other, typename std::enable_if<
                std::is_convertible<URange, TAdaptee>::value>::type* = 0)
                : m_adapter_body(p_other.m_adapter_body) {}
        };

        namespace detail {
            template<class TPredicate> struct where_dispatch {
                typedef typename std::decay<TPredicate>::type predicate_type;
                TPredicate m_predicate;

                explicit where_dispatch(TPredicate p_predicate)
                    : m_predicate(p_predicate) {}
            };

            template<class TRange, class TPredicate> 
            auto operator | (TRange&& p_range, where_dispatch<TPredicate> const & p_dispatch) 
                -> adapter::where_adapter<typename std::decay<TRange>::type, typename std::decay<TPredicate>::type>
            {
                return adapter::where_adapter<
                    typename std::decay<TRange>::type, 
                    typename std::decay<TPredicate>::type
                >(std::forward<TRange>(p_range), p_dispatch.m_predicate);
            }
        }
    }

    template<class TPredicate> auto where(TPredicate p_predicate)
        -> adapter::detail::where_dispatch<TPredicate>
    {
        return adapter::detail::where_dispatch<TPredicate>(p_predicate);
    }
}}